/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.google.code.japidiff;

import java.util.Collection;
import java.util.List;
import java.util.LinkedList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Arrays;

import org.objectweb.asm.Opcodes;

public class Printer {

    public boolean publicApis;
    public String classNames;
    public boolean ordered = true;

    public void printClass(JavaClass clazz, StringBuilder builder) {
        builder.append("package ").append(clazz.getName().substring(0, clazz.getName().lastIndexOf('/')).replace('/', '.')).append(";\n");
        printClass(clazz, builder, "");
    }

    public void printClasses(Collection<JavaClass> classes, StringBuilder builder) {
        List<JavaClass> list = new LinkedList<JavaClass>();
        for (JavaClass cl : classes) {
            // Ignore compiler generated methods
            if ((cl.getAccess() & Opcodes.ACC_SYNTHETIC) != 0) {
                continue;
            }
            // Do not print non public apis if requested
            if ((cl.getAccess() & (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED)) == 0 && publicApis) {
                continue;
            }
            if (cl.isInner()) {
                continue;
            }
            if (classNames != null && !cl.getName().matches(classNames)) {
                continue;
            }
            list.add(cl);
        }
        Collections.sort(list, new Comparator<JavaClass>() {
            public int compare(JavaClass o1, JavaClass o2) {
                return o1.getName().compareTo(o2.getName());
            }
        });
        for (JavaClass cl : list) {
            printClass(cl, builder);
            builder.append("\n");
        }
    }

    private void printClass(JavaClass clazz, StringBuilder builder, String indent) {
        if (clazz.getAnnotations() != null && clazz.getAnnotations().length > 0) {
            List<JavaAnnotation> annotations = Arrays.asList(clazz.getAnnotations());
            if (ordered) {
                Collections.sort(annotations, new Comparator<JavaAnnotation>() {
                    public int compare(JavaAnnotation o1, JavaAnnotation o2) {
                        return o1.getDesc().compareTo(o2.getDesc());
                    }
                });
            }
            for (JavaAnnotation annotation : annotations) {
                builder.append(indent);
                printAnnotation(annotation, builder);
                builder.append("\n");
            }
        }
        builder.append(indent);
        if ((clazz.getAccess() & Opcodes.ACC_PUBLIC) != 0) {
            builder.append("public ");
        } else if ((clazz.getAccess() & Opcodes.ACC_PRIVATE) != 0) {
            builder.append("private ");
        } else if ((clazz.getAccess() & Opcodes.ACC_PROTECTED) != 0) {
            builder.append("protected ");
        }
        if ((clazz.getAccess() & Opcodes.ACC_STATIC) != 0) {
            builder.append("static ");
        }
        if ((clazz.getAccess() & Opcodes.ACC_FINAL) != 0 && (clazz.getAccess() & Opcodes.ACC_ENUM) == 0) {
            builder.append("final ");
        }

        String[] itfs = clazz.getInterfaces();
        String parent = clazz.getSuperName();
        if ((clazz.getAccess() & Opcodes.ACC_INTERFACE) != 0) {
            if (itfs != null && itfs.length == 1 && itfs[0].equals("java/lang/annotation/Annotation")) {
                builder.append("@interface");
                itfs = null;
            } else {
                builder.append("interface");
            }
        } else if ((clazz.getAccess() & Opcodes.ACC_ENUM) != 0) {
            builder.append("enum");
            if (parent != null && parent.equals("java/lang/Enum")) {
                parent = null;
            }
        } else {
            builder.append("class");
        }
        builder.append(' ');
        String name = clazz.getName().substring(clazz.getName().lastIndexOf('/') + 1);
        if (name.indexOf('$') > 0 && clazz.isInner()) {
            name = name.substring(name.indexOf('$') + 1);
        }
        builder.append(name);
        if (clazz.getSignature() != null && clazz.getSignature().startsWith("<")) {
            builder.append('<');
            String s = clazz.getSignature();
            s = s.substring(1, s.indexOf('>'));
            printTemplate(s, builder);
            builder.append('>');
        }
        builder.append(" ");
        if (parent != null && !parent.equals("java/lang/Object")) {
            builder.append("extends ").append(clazz.getSuperName().replace('/', '.')).append(" ");
        }
        if (itfs != null && itfs.length > 0) {
            if ((clazz.getAccess() & Opcodes.ACC_INTERFACE) != 0) {
                builder.append("extends ");
            } else {
                builder.append("implements ");
            }
            if (clazz.getSignature() != null) {
                String s = clazz.getSignature();
                // skip template
                if (s.startsWith("<")) {
                    s = s.substring(s.indexOf(">") + 1);
                }
                // skip extends
                int i = s.indexOf(';');
                int j = s.indexOf('<');
                if (j > 0 && j < i) {
                    i = s.indexOf('<', j);
                }
                s = s.substring(i + 1);
                printTypes(s, builder);
                builder.append(' ');
            } else {
                for (int i = 0; i < itfs.length; i++) {
                    builder.append(itfs[i].replace('/', '.'));
                    if (i < itfs.length - 1) {
                        builder.append(',');
                    }
                    builder.append(' ');
                }
            }
        }
        builder.append("{\n");

        if (clazz.getFields() != null) {
            List<JavaField> fields = Arrays.asList(clazz.getFields());
            if (ordered) {
                Collections.sort(fields, new Comparator<JavaField>() {
                    public int compare(JavaField o1, JavaField o2) {
                        int i = o1.getName().compareTo(o2.getName());
                        if (i == 0) {
                            return o1.getDesc().compareTo(o2.getDesc());
                        } else {
                            return i;
                        }
                    }
                });
            }
            for (JavaField field : fields) {
                // Ignore compiler generated methods
                if ((field.getAccess() & Opcodes.ACC_SYNTHETIC) != 0) {
                    continue;
                }
                // Do not print non public apis if requested
                if ((field.getAccess() & (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED)) == 0 && publicApis) {
                    continue;
                }
                // Print annotations
                List<JavaAnnotation> annotations = Arrays.asList(field.getAnnotations());
                if (ordered) {
                    Collections.sort(annotations, new Comparator<JavaAnnotation>() {
                        public int compare(JavaAnnotation o1, JavaAnnotation o2) {
                            return o1.getDesc().compareTo(o2.getDesc());
                        }
                    });
                }
                for (JavaAnnotation annotation : annotations) {
                    if (annotation.isVisible()) {
                        builder.append(indent).append("    ");
                        printAnnotation(annotation, builder);
                        builder.append("\n");
                    }
                }
                builder.append(indent).append("    ");
                if ((field.getAccess() & Opcodes.ACC_PUBLIC) != 0) {
                    if ((clazz.getAccess() & Opcodes.ACC_INTERFACE) == 0) {
                        builder.append("public ");
                    }
                } else if ((field.getAccess() & Opcodes.ACC_PRIVATE) != 0) {
                    builder.append("private ");
                } else if ((field.getAccess() & Opcodes.ACC_PROTECTED) != 0) {
                    builder.append("protected ");
                }
                if ((field.getAccess() & Opcodes.ACC_STATIC) != 0) {
                    if ((clazz.getAccess() & Opcodes.ACC_INTERFACE) == 0) {
                        builder.append("static ");
                    }
                }
                if ((field.getAccess() & Opcodes.ACC_FINAL) != 0) {
                    if ((clazz.getAccess() & Opcodes.ACC_INTERFACE) == 0) {
                        builder.append("final ");
                    }
                }
                printType(field.getSignature() != null ? field.getSignature() : field.getDesc(), builder);
                builder.append(" ");
                builder.append(field.getName());
                if (false && field.getValue() != null) {
                    builder.append(" = ");
                    if (field.getValue() instanceof String) {
                        builder.append('"').append(field.getValue()).append('"');
                    } else if (field.getValue() instanceof org.objectweb.asm.Type) {
                        printType(field.getValue().toString(), builder);
                        builder.append(".class");
                    } else {
                        builder.append(field.getValue());
                    }
                }
                builder.append(";\n");
            }
        }

        if (clazz.getMethods() != null) {
            List<JavaMethod> methods = Arrays.asList(clazz.getMethods());
            if (ordered) {
                Collections.sort(methods, new Comparator<JavaMethod>() {
                    public int compare(JavaMethod o1, JavaMethod o2) {
                        int i = o1.getName().compareTo(o2.getName());
                        if (i == 0) {
                            return o1.getDesc().compareTo(o2.getDesc());
                        } else {
                            return i;
                        }
                    }
                });
            }
            for (JavaMethod mth : methods) {
                // Ignore compiler generated methods
                if ((mth.getAccess() & Opcodes.ACC_SYNTHETIC) != 0) {
                    continue;
                }
                // Do not print non public apis if requested
                if ((mth.getAccess() & (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED)) == 0 && publicApis) {
                    continue;
                }
                // Print annotations
                List<JavaAnnotation> annotations = Arrays.asList(mth.getAnnotations());
                if (ordered) {
                    Collections.sort(annotations, new Comparator<JavaAnnotation>() {
                        public int compare(JavaAnnotation o1, JavaAnnotation o2) {
                            return o1.getDesc().compareTo(o2.getDesc());
                        }
                    });
                }
                for (JavaAnnotation annotation : annotations) {
                    if (annotation.isVisible()) {
                        builder.append(indent).append("    ");
                        printAnnotation(annotation, builder);
                        builder.append("\n");
                    }
                }
                builder.append(indent).append("    ");
                if ((mth.getAccess() & Opcodes.ACC_PUBLIC) != 0) {
                    if ((clazz.getAccess() & Opcodes.ACC_INTERFACE) == 0) {
                        builder.append("public ");
                    }
                } else if ((mth.getAccess() & Opcodes.ACC_PRIVATE) != 0) {
                    builder.append("private ");
                } else if ((mth.getAccess() & Opcodes.ACC_PROTECTED) != 0) {
                    builder.append("protected ");
                }
                if ((mth.getAccess() & Opcodes.ACC_STATIC) != 0) {
                    builder.append("static ");
                }
                if ((mth.getAccess() & Opcodes.ACC_FINAL) != 0) {
                    builder.append("final ");
                }
                boolean abst = (mth.getAccess() & Opcodes.ACC_ABSTRACT) != 0;
                if (abst) {
                    if ((clazz.getAccess() & Opcodes.ACC_INTERFACE) == 0) {
                        builder.append("abstract ");
                    }
                }
                if (mth.getSignature() != null) {
                    String s = mth.getSignature();
                    if (s.startsWith("<")) {
                        s = s.substring(1, s.indexOf('>'));
                        builder.append("<");
                        printTemplate(s, builder);
                        builder.append("> ");
                    }
                }
                if (mth.getName().equals("<init>")) {
                    String n = clazz.getName().substring(clazz.getName().lastIndexOf('/') + 1);
                    if (n.indexOf('$') > 0 && clazz.isInner()) {
                        n = n.substring(n.indexOf('$') + 1);
                    }
                    builder.append(n);
                } else {
                    String ret = mth.getSignature() != null ? mth.getSignature() : mth.getDesc();
                    ret = ret.substring(ret.indexOf(')') + 1);
                    printType(ret, builder);
                    builder.append(' ');
                    builder.append(mth.getName());
                }
                builder.append("(");
                String paramStr = mth.getSignature() != null ? mth.getSignature() : mth.getDesc();
                paramStr = paramStr.substring(paramStr.indexOf('(') + 1, paramStr.indexOf(')'));
                if (paramStr.length() > 0) {
                    printTypes(paramStr, builder);
               }
                builder.append(")");
                for (int j = 0; j < mth.getAnnotations().length; j++) {
                    JavaAnnotation annotation = mth.getAnnotations()[j];
                    if (annotation.isDefault()) {
                        builder.append(" default ");
                        if (annotation.getValue() != null) {
                            if (annotation.getValue() instanceof String) {
                                builder.append('"').append(annotation.getValue()).append('"');
                            } else if (annotation.getValue() instanceof org.objectweb.asm.Type) {
                                printType(annotation.getValue().toString(), builder);
                                builder.append(".class");
                            } else {
                                builder.append(annotation.getValue());
                            }
                        } else if (annotation.getValues().length == 1) {
                            printAnnotationValue(annotation.getValues()[0], builder, false);
                        } else if (mth.getDesc().charAt(mth.getDesc().indexOf(')') + 1) == '[') {
                            builder.append("{ }");
                        } else {
                            builder.append("");
                        }
                    }
                }
                if (mth.getExceptions() != null && mth.getExceptions().length > 0) {
                    builder.append(" throws ");
                    for (int j = 0; j < mth.getExceptions().length; j++) {
                        if (j > 0) {
                            builder.append(", ");
                        }
                        printClassName(mth.getExceptions()[j], builder);
                    }
                }
                builder.append(";\n");
            }
        }
        // Print inner classes
        List<JavaClass> innerClasses = Arrays.asList(clazz.getInnerClasses());
        if (ordered) {
            Collections.sort(innerClasses, new Comparator<JavaClass>() {
                public int compare(JavaClass o1, JavaClass o2) {
                    return o1.getName().compareTo(o2.getName());
                }
            });
        }
        for (JavaClass cl : innerClasses) {
            // Ignore compiler generated methods
            if ((cl.getAccess() & Opcodes.ACC_SYNTHETIC) != 0) {
                continue;
            }
            // Do not print non public apis if requested
            if ((cl.getAccess() & (Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED)) == 0 && publicApis) {
                continue;
            }
            printClass(cl, builder, indent + "    ");
        }

        builder.append(indent).append("}\n");
    }

    void printTypes(String s, StringBuilder builder) {
        for (int j = 0; j < s.length();) {
            int k = j;
            if (j > 0) {
                builder.append(", ");
            }
            if (s.charAt(k) == '[') {
                k++;
            }
            if (s.charAt(k) == 'L' || s.charAt(k) == 'T') {
                k = getEndIndexOfComplexType(s, k);
                printType(s.substring(j, k + 1), builder);
            } else {
                printType(s.substring(j, k + 1), builder);
            }
            j = k + 1;
        }
    }

    private int getEndIndexOfComplexType(String s, int i) {
        int nb = 0;
        for (;;) {
            char c = s.charAt(i);
            if (c == ';' && nb == 0) {
                return i;
            } else if (c == '<') {
                nb++;
            } else if (c == '>') {
                nb--;
            }
            i++;
        }
    }

    private void printTemplate(String s, StringBuilder builder) {
        for (int i = 0; i < s.length();) {
            if (i > 0) {
                builder.append(", ");
            }
            int j = s.indexOf(';', i);
            int k = s.indexOf('<', i);
            if (k > 0 && k < j) {
                j = s.indexOf(';', s.indexOf('>', k));
            }
            String t = s.substring(i, j + 1);
            /// ZZZZ
            if (t.indexOf(':') > 0) {
                builder.append(t.substring(0, t.indexOf(':')));
                t = t.substring(t.lastIndexOf(':') + 1);
                if (!t.equals("Ljava/lang/Object;")) {
                    builder.append(" extends ");
                    printType(t, builder);
                }
            } else {
                builder.append(t);
            }
            i = j + 1;
        }
    }

    private void printAnnotation(JavaAnnotation annotation, StringBuilder builder) {
        String name = annotation.getDesc();
        name = name.substring(1, name.length() - 1).replace('/', '.');
        builder.append("@");
        //if (name.startsWith("java.lang.")) {
        //	name = name.substring("java.lang.".length());
        //}
        builder.append(name);
        if (annotation.getValues().length > 0) {
            builder.append('(');
            for (int j = 0; j < annotation.getValues().length; j++) {
                JavaAnnotation.AnnotationValue v = annotation.getValues()[j];
                if (j > 0) {
                    builder.append(", ");
                }
                printAnnotationValue(v, builder, true);
            }
            builder.append(')');
        } else if (annotation.getAnnotations().length > 0) {
            builder.append('(');
            for (int j = 0; j < annotation.getAnnotations().length; j++) {
                JavaAnnotation a = annotation.getAnnotations()[j];
                if (j > 0) {
                    builder.append(", ");
                }
                builder.append(a.getName()).append(" = {");
                for (int k = 0; k < a.getValues().length; k++) {
                    JavaAnnotation.AnnotationValue v = a.getValues()[k];
                    if (k > 0) {
                        builder.append(", ");
                    }
                    builder.append(v.getDesc().substring(1, v.getDesc().length() - 1).replace('/', '.')).append('.').append(v.getValue());
                }
                builder.append("}");
            }
            builder.append(')');
        }
    }

    private void printAnnotationValue(JavaAnnotation.AnnotationValue v, StringBuilder builder, boolean printName) {
        if (printName) {
            builder.append(v.getName()).append(" = ");
        }
        if (v.getDesc() != null && v.getDesc().startsWith("L")) {
            builder.append(v.getDesc().substring(1, v.getDesc().length() - 1).replace('/', '.')).append('.').append(v.getValue());
        } else {
            builder.append(v.getValue());
        }
    }

    void printType(String ret, StringBuilder builder) {
        boolean array = ret.startsWith("[");
        if (ret.endsWith(";")) {
            ret = ret.substring(0, ret.length() - 1);
        }
        if (array) {
            ret = ret.substring(1);
        }
        if (ret.equals("V")) {
            ret = "void";
        } else if (ret.startsWith("L")) {
            ret = ret.substring(1);
            if (ret.indexOf('<') > 0) {
                printClassName(ret.substring(0, ret.indexOf('<')), builder);
                builder.append("<");
                ret = ret.substring(ret.indexOf('<') + 1, ret.lastIndexOf('>'));
                if (ret.startsWith("+")) {
                    builder.append("? extends ");
                    ret = ret.substring(1);
                }
                printTypes(ret, builder);
                builder.append(">");
            } else {
                printClassName(ret, builder);
            }
            if (array) {
                builder.append("[]");
            }
            return;
        } else if (ret.startsWith("T")) {
            ret = ret.substring(1);
            builder.append(ret);
            if (array) {
                builder.append("[]");
            }
            return;
        } else if (ret.equals("I")) {
            ret = "int";
        } else if (ret.equals("J")) {
            ret = "long";
        } else if (ret.equals("S")) {
            ret = "short";
        } else if (ret.equals("F")) {
            ret = "float";
        } else if (ret.equals("D")) {
            ret = "double";
        } else if (ret.equals("C")) {
            ret = "char";
        } else if (ret.equals("Z")) {
            ret = "boolean";
        } else if (ret.equals("B")) {
            ret = "byte";
        } else if (ret.equals("*")) {
            ret = "?";
        } else {
            ret = ret;
        }
        builder.append(ret);
        if (array) {
            builder.append("[]");
        }
    }

    private void printClassName(String ret, StringBuilder builder) {
        ret = ret.replace('/', '.');
        ret = ret.replace('$', '.');
        if (ret.substring(0, ret.lastIndexOf('.') + 1).equals("java.lang.")) {
            ret = ret.substring("java.lang.".length());
        }
        builder.append(ret);
    }

}
